package fr.loof.logeek;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.lang.reflect.Modifier;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.bytecode.AccessFlag;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;

import org.codehaus.plexus.logging.AbstractLogEnabled;

/**
 * @author <a href="mailto:nicolas@apache.org">Nicolas De Loof</a>
 */
public abstract class AbstractLoggingCodeTransformer
    extends AbstractLogEnabled
    implements CodeTransformer
{
    protected ClassPool cp;

    private static String[] DEFAULT_METHOD_EXCLUDES = { "get.*", "set.*", "is.*", "toString", "equals", "hashCode" };

    private String[] methodExcludes = DEFAULT_METHOD_EXCLUDES;

    public boolean instrumentClass( CtClass cl, ClassPool classPool )
        throws Exception
    {
        this.cp = classPool;

        if ( cl.isInterface() || cl.isEnum() )
        {
            return false;
        }
        insertLogger( cl );

        for ( CtMethod method : cl.getDeclaredMethods() )
        {
            getLogger().debug( "Trying to instrument " + cl.getName() + "#" + method.getName() );
            if ( toBeInstrumented( method ) )
            {
                insertInlogging( method );
                insertExceptionLogging( method );
                insertOutLogging( method );
            }
        }
        return true;
    }

    protected abstract void insertLogger( CtClass cl )
        throws Exception;

    protected abstract void insertInlogging( CtMethod method )
        throws Exception;

    protected abstract void log( StringBuffer sb, String level, String data );

    protected String getMethodParameterName( CtMethod method, int i )
    {
        String name = "?";
        int offset = Modifier.isStatic( method.getModifiers() ) ? 0 : 1;
        CodeAttribute ca = method.getMethodInfo().getCodeAttribute();
        if ( ca == null )
        {
            // empty method
            return name;
        }
        LocalVariableAttribute va = (LocalVariableAttribute) ca.getAttribute( LocalVariableAttribute.tag );
        if ( va != null )
        {
            for ( int j = 0; j < va.tableLength(); j++ )
            {
                if ( va.index( j ) == i + offset )
                {
                    name = va.variableName( j );
                    break;
                }
            }
        }
        return name;
    }

    protected abstract void insertExceptionLogging( CtMethod method )
        throws Exception;

    protected abstract void insertOutLogging( CtMethod method )
        throws Exception;

    private boolean toBeInstrumented( CtMethod method )
    {
        int modifiers = method.getModifiers();
        if ( isSynthetic( modifiers ) || isAbstract( modifiers ) || isNative( modifiers ) )
        {
            return false;
        }
        if ( method.getMethodInfo().isConstructor() )
        {
            return false;
        }
        if ( method.getMethodInfo().isStaticInitializer() )
        {
            return false;
        }
        String name = method.getName();
        if ( name.contains( "$" ) )
        {
            return false;
        }
        for ( String exclude : methodExcludes )
        {
            if ( exclude.matches( name ) )
            {
                return false;
            }
        }
        return !method.isEmpty();
    }

    private boolean isSynthetic( int modifiers )
    {
        return ( modifiers & AccessFlag.SYNTHETIC ) != 0;
    }

    private boolean isNative( int modifiers )
    {
        return ( modifiers & AccessFlag.NATIVE ) != 0;
    }

    private boolean isAbstract( int modifiers )
    {
        return ( modifiers & AccessFlag.ABSTRACT ) != 0;
    }
}
